﻿namespace DataDuctus.MongoDb.Serialization
{
	using System;
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.ComponentModel.DataAnnotations;
	using System.Diagnostics;
	using System.Linq;
	using System.Reflection;
	using System.Runtime.Serialization;

	using DataDuctus.MongoDb.Serialization.Extensions;
	using DataDuctus.MongoDb.Serialization.Utils;

	using MongoDB.Bson;
	using MongoDB.Bson.Serialization;

	public class DataContractMapper<T>
	{
		#region Public Static Properties
		public static bool IsRegistered
		{
			get
			{
				return BsonClassMap.IsClassMapRegistered(typeof(T));
			}
		}
		#endregion // Public Static Properties

		#region Public Methods
		public IEnumerable<Type> Register()
		{
			if (IsRegistered)
			{
				return StaticDefaults.EmptyTypeEnumerable; // Already registered
			}

			var registeredList = new List<Type>();

			// Always register root to bottom
			var typeHierchy = typeof(T).GetDataContractInheritanceChain();

			foreach (var type in typeHierchy.Where(type => !BsonClassMap.IsClassMapRegistered(type)))
			{
				if (type.Equals(typeof(T)))
				{
					registeredList.Add(typeof(T));

					var rest = InternalRegister();

					foreach (var t in rest)
					{
						foreach (var regType in t.RegisterType().Where(regType => !registeredList.Contains(regType)))
						{
							registeredList.Add(regType);
						}
					}
				}
				else
				{
					foreach (var regType in type.RegisterType().Where(regType => !registeredList.Contains(regType)))
					{
						registeredList.Add(regType);
					}
				}
			}

			return registeredList;
		}

		#endregion // Public Methods

		#region Private Methods
		private static IEnumerable<Type> InternalRegister()
		{
			Type myType = typeof(T);

			var dataContract = myType.GetAttribute<DataContractAttribute>();

			if (null == dataContract)
			{
				throw new Exception(string.Format("The type = {0} is not decorated with [DataContract]", myType.Name));
			}

			var needsRegister = new List<Type>();

			BsonClassMap.RegisterClassMap<T>(
				cm =>
				{
					//
					// Class Mapping
					//
					if (!string.IsNullOrEmpty(dataContract.Name))
					{
						cm.SetDiscriminator(dataContract.Name);
					}

					// Register known types (if any)
					var knownTypes = MapKnownTypes(cm);

					needsRegister.AddRange(from item in knownTypes where !BsonClassMap.IsClassMapRegistered(item) select item);

					//
					// Property / Field Mapping
					//
					var methods = myType.ToShouldSerializeMethods();

					// Enum DataMember Properties
					foreach (var pinfo in myType.ToSerializableProperties())
					{
						MapMemberInfo(
							pinfo,
							cm,
							pinfo.HasAttribute<KeyAttribute>(),
							(from meth in methods where meth.PropertyName == pinfo.Name select meth.Method).FirstOrDefault());

						AddNeedsRegister(pinfo.PropertyType, needsRegister);
					}

					// Enum DataMember Fields
					foreach (var field in myType.ToSerializableFields())
					{
						MapMemberInfo(
							field,
							cm,
							field.HasAttribute<KeyAttribute>(),
							(from meth in methods where meth.PropertyName == field.Name select meth.Method).FirstOrDefault());

						AddNeedsRegister(field.FieldType, needsRegister);
					}
				});

			return (from item in needsRegister where !BsonClassMap.IsClassMapRegistered(item) select item).ToArray();
		}

		private static void MapMemberInfo(MemberInfo member, BsonClassMap<T> cm, bool isKey,MethodInfo shouldSerializeMethod)
		{
			var dataMember = member.GetAttribute<DataMemberAttribute>();
			var defaultValue = member.GetAttribute<DefaultValueAttribute>();

			if (null == dataMember)
			{
				return; // Skip element
			}

			if (member.HasBaseTypeAttribute<DataMemberAttribute>())
			{
				if (null != defaultValue)
				{
					Trace.TraceWarning(
						string.Format(
							"DefaultValue value = {0} is ignored since base type has a DataMemberAttribute defined", defaultValue.Value));
				}

				return; // Skip element (already defined in base)
			}

			var cmMember = cm.MapMember(member);			

			if (!string.IsNullOrEmpty(dataMember.Name))
			{
				cmMember.SetElementName(dataMember.Name);
			}

			if (dataMember.Order >= 0)
			{
				cmMember.SetOrder(dataMember.Order);
			}

			if (isKey)
			{
				cm.SetIdMember(cmMember);
				cmMember.SetIdGenerator(member.GetIdGenerator());
			}

			if (!dataMember.EmitDefaultValue)
			{
				cmMember.SetIgnoreIfNull(true);
			}			

			if (null != defaultValue)
			{
				// TODO: Need to check if default value is either null or zero it shall follow 
				// TODO: dataMember.EmitDefaultValue otherwise true (as second parameter)!
				cmMember.SetDefaultValue(defaultValue.Value); 
			}

			if (dataMember.IsRequired)
			{
				cmMember.SetIsRequired(true);
			}

			var representation = member.TypeConverterToBsonRepresentation();
			if (BsonType.EndOfDocument != representation)
			{
				cmMember.SetRepresentation(representation);
			}
			else
			{
				DefaultMappingAlterations(cmMember, member);
			}

			if (null != shouldSerializeMethod)
			{
				cmMember.SetShouldSerializeMethod(obj => (bool)shouldSerializeMethod.Invoke(obj, null));
			}

			// TODO: Serialization options: DateTime (typeconverter?), DictionarySerializationOptions
		}

		private static void AddNeedsRegister(Type type, List<Type> needsRegister)
		{
			if (null == type.GetAttribute<DataContractAttribute>())
			{
				return;
			}

			if (!needsRegister.Contains(type) && !BsonClassMap.IsClassMapRegistered(type))
			{
				needsRegister.Add(type);
			}
		}

		private static void DefaultMappingAlterations(BsonMemberMap cmMember, MemberInfo member)
		{
			var type = member.ExtractType();

			if (type.Equals(typeof(uint)))
			{
				cmMember.SetRepresentation(BsonType.Int64);
			}

			if (type.Equals(typeof(ulong)))
			{
				cmMember.SetRepresentation(BsonType.String);
			}
		}

		private static IEnumerable<Type> MapKnownTypes(BsonClassMap<T> cm)
		{
			var myType = typeof(T);
			var isRootType = myType.IsKnownTypesRootType();

			var list = myType.ToKnownTypes().ToList();

			var listRegularRegistration = list.Where(type => !type.IsInInheritanceChain(myType)).ToList();

			foreach (var type in listRegularRegistration)
			{
				list.Remove(type);
			}

			if (list.Count > 0 && isRootType)
			{
				cm.SetIsRootClass(true);
				cm.SetDiscriminatorIsRequired(true);
			}

			if (!isRootType)
			{
				return StaticDefaults.EmptyTypeEnumerable; // Nothing to do
			}			

			foreach (var type in list)
			{
				cm.AddKnownType(type);
			}

			list.AddRange(listRegularRegistration);

			return list;
		}
		#endregion // Private Methods
	}
}